package util

import (
	"bufio"
	"bytes"
	"fmt"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"gopkg.in/yaml.v2"
	"io"
	"io/ioutil"
	ckeadmv1beta1 "iop.inspur.com/api/v1alpha1"
	corev1 "k8s.io/api/core/v1"
	kuberuntime "k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/runtime/serializer"
	utilyaml "k8s.io/apimachinery/pkg/util/yaml"
	"os"
	"path/filepath"
	"reflect"
	"strings"
)

type GVKContent struct {
	GVK     schema.GroupVersionKind
	Content []byte
}

type GvkSlice []GVKContent

var (
	scheme = kuberuntime.NewScheme()
	codec  kuberuntime.Codec
)

func init() {
	ckeadmv1beta1.AddToScheme(scheme)     // registry
	corev1.AddToScheme(scheme)            // corev1
	codec = GetCkeadmDefaultCodec(scheme) // codec
}

type ckeadmcodec struct {
	kuberuntime.Encoder
	kuberuntime.Decoder
}

func NewCkeadmCodec(encoder kuberuntime.Encoder, decoder kuberuntime.Decoder) kuberuntime.Codec {
	return &ckeadmcodec{encoder, decoder}
}

func GetCkeadmDefaultCodec(scheme *kuberuntime.Scheme) kuberuntime.Codec {
	var codecfactory = serializer.NewCodecFactory(scheme)
	var infoer, _ = kuberuntime.SerializerInfoForMediaType(codecfactory.SupportedMediaTypes(), kuberuntime.ContentTypeYAML)

	return NewCkeadmCodec(infoer.Serializer, codecfactory.UniversalDecoder())
}

func GetCkePatch(path string) (*[]ckeadmv1beta1.CkePatch, error) {
	var ckePatches = new([]ckeadmv1beta1.CkePatch)

	err := GetCkePatchFromFile(path, ckePatches)
	if err != nil {
		logrus.Errorf("failed to read ckeNodes from file, %s\n", err.Error())
		return ckePatches, err
	}

	return ckePatches, nil
}

func GetCkePatchFromFile(path string, ckePatches *[]ckeadmv1beta1.CkePatch) (err error) {
	var context []byte
	var gvkSlice = new(GvkSlice)
	var files []string

	if files, err = ListAllFilesInPath(path); err != nil {
		return err
	}

	// read files to gvk content slice
	for _, file := range files {
		if context, err = ioutil.ReadFile(file); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
		if err = SplitYAMLDocuments(context, gvkSlice); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
	}

	return DocumentSliceToCkePatch(gvkSlice, ckePatches)
}

func ListAllFilesInPath(filedir string) (files []string, err error) {
	err = filepath.Walk(filedir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if !info.IsDir() {
			files = append(files, path)
		}
		return nil
	})

	if err == nil && len(files) == 0 {
		err = fmt.Errorf("no files find in path %s", filedir)
	}
	return
}

func SplitYAMLDocuments(yamls []byte, gvkSlice *GvkSlice) error {
	reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewBuffer(yamls)))
	for {
		typeMetaInfo := kuberuntime.TypeMeta{}
		// Read one YAML document at a time, until io.EOF is returned
		b, err := reader.Read()
		if err == io.EOF {
			break
		} else if err != nil {
			return err
		}

		if len(strings.ReplaceAll(string(b), "\n", "")) == 0 {
			continue
		}

		// Deserialize the TypeMeta information of this byte slice
		if err := yaml.Unmarshal(b, &typeMetaInfo); err != nil {
			return err
		}

		// Require TypeMeta information to be present
		if len(typeMetaInfo.APIVersion) == 0 || len(typeMetaInfo.Kind) == 0 {
			return errors.New("invalid configuration: kind and apiVersion is mandatory information that needs to be specified in all YAML documents")
		}

		// Build a GroupVersionKind object from the deserialized TypeMeta object
		gv, err := schema.ParseGroupVersion(typeMetaInfo.APIVersion)
		if err != nil {
			return errors.Wrap(err, "unable to parse apiVersion")
		}
		gvk := gv.WithKind(typeMetaInfo.Kind)

		// Save the mapping between the gvk and the bytes that object consists of
		*gvkSlice = append(*gvkSlice, GVKContent{GVK: gvk, Content: b})
	}

	return nil
}

func DocumentSliceToCkePatch(gvkslice *GvkSlice, ckePatches *[]ckeadmv1beta1.CkePatch) error {
	if err := DocumentListToObjects(gvkslice); err != nil {
		return err
	}

	for _, gvkcontent := range *gvkslice {
		if scheme.AllKnownTypes()[gvkcontent.GVK] == nil {
			return fmt.Errorf("no kind %s is registered for version %s", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
		switch reflect.New(scheme.AllKnownTypes()[gvkcontent.GVK]).Interface().(type) {
		case *ckeadmv1beta1.CkePatch:
			var ckePatch ckeadmv1beta1.CkePatch
			if err := kuberuntime.DecodeInto(codec, gvkcontent.Content, &ckePatch); err != nil {
				return err
			}
			*ckePatches = append(*ckePatches, ckePatch)
		}
	}
	return nil
}

func DocumentListToObjects(gvkslice *GvkSlice) error {
	for item, gvkcontent := range *gvkslice {
		if scheme.AllKnownTypes()[gvkcontent.GVK] == nil {
			continue
		}
		switch reflect.New(scheme.AllKnownTypes()[gvkcontent.GVK]).Interface().(type) {
		case *corev1.List:
			var list = new(corev1.List)
			if err := kuberuntime.DecodeInto(codec, gvkcontent.Content, list); err != nil {
				return err
			}
			*gvkslice = append((*gvkslice)[:item], (*gvkslice)[item+1:]...)
			for _, object := range list.Items {
				if err := SplitYAMLDocuments(object.Raw, gvkslice); err != nil {
					return err
				}
			}
			return DocumentListToObjects(gvkslice)
		}
	}

	return nil
}
